/*
 * jdbc-trace-wrapper
 * Copyright (C) 2010. The jdbc-trace-wrapper Project
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package nz.jdbcwrapper;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;

public class WrapperDriver implements Driver, DataSource
{
  static final String URL_KEY       = "url";
  static final String CLASS_KEY     = "class";
  static final String TRACE_KEY     = "trace";
  static final String TIMEOUT_KEY   = "timeout";
  static final String PRINT_LENGTH_KEY = "printLength";
  static final String MAX_ARRAY_KEY  = "items";
  static final String TRACE_FILE_KEY = "tracefile";

  static final int MAJORVERSION = 1;
  static final int MINORVERSION = 0;

  static final String urlPrefix = "jdbc:wrapper:";
  static final Set driverClasses = new HashSet();

  static volatile Driver realDriver = null;

  static final PrintWriter DEFAULT_OUTPUT = new PrintWriter(System.err, true);
  static PrintWriter out = DEFAULT_OUTPUT;

  static Data connectionData = new Data();
  int loginTimeout = Integer.MAX_VALUE;

  static
  {
    try
    {
      DriverManager.registerDriver(new WrapperDriver());

    }
    catch (java.sql.SQLException e)
    {
      throw new RuntimeException("Can't register WrapperDriver: " + e);
    }
  }

  /**
   * Gets the driver's major version number
   *
   * @return the driver's major version number
   */
  public int getMajorVersion()
  {
    if (realDriver == null)
    {
      Util.trace("wrapper getMajorVersion() -> " + MAJORVERSION);
      return MAJORVERSION;
    }
    long key = Util.traceBegin("getMajorVersion()");
    int result = realDriver.getMajorVersion();
    Util.traceEnd(key, String.valueOf(result));
    return result;
  }


  /**
   * Get the driver's minor version number
   *
   * @return the driver's minor version number
   */
  public int getMinorVersion()
  {
    if (realDriver == null)
    {
      Util.trace("wrapper getMinorVersion() -> " + MINORVERSION);
      return MINORVERSION;
    }
    long key = Util.traceBegin("getMinorVersion()");
    int result = realDriver.getMinorVersion();
    Util.traceEnd(key, String.valueOf(result));
    return result;
  }

  public DriverPropertyInfo[] getPropertyInfo(String url, Properties info)
    throws SQLException
  {
    if (realDriver == null)
    {
      Util.trace("wrapper getPropertyInfo() -> DriverPropertyInfo[0]");
      return new DriverPropertyInfo[0];
    }
    long key = Util.traceBegin("getMinorVersion()");
    DriverPropertyInfo[] result = realDriver.getPropertyInfo(url, info);

    Util.traceEnd(key, result == null ? "null" : String.valueOf(result.length));
    int length = result == null ? 0 : result.length;
    for (int i = 0; i < length; ++i)
    {
      if (result[i].name != null)
        Util.trace("name[" + i + "] = " + result[i].name);

      if (result[i].description != null)
        Util.trace("description[" + i + "] = " + result[i].description);

      if (result[i].value != null)
        Util.trace("value[" + i + "] = " + result[i].value);

      Util.trace("required[" + i + "] = " + result[i].required);

      if (result[i].choices != null)
      {
        Util.trace("choices[" + i + "] = String[" + result[i].choices.length + "]");

        for (int j = 0; j < result[i].choices.length; ++j)
        {
          Util.trace("choices[" + i + "][" + j + "] = " + result[i].choices[j]);
        }
      }
    }
    return result;
  }

  public boolean acceptsURL(String url)
  {
    boolean accept = parseUrl(url, null) != null;
    Util.trace("WrapperDriver: accept url \"" + url + "\"? " + accept);
    return accept;
  }

  public Connection connect(String url, Properties info)
    throws SQLException
  {
    Util.trace("WrapperDriver: url is \"" + url + '"');
    Data data = parseUrl(url, info);
    if (data == null)
      return null;

    connectionData = data;
    return connect(data, info);
  }

  private WrapperConnection connect(Data data, Properties info)
    throws SQLException
  {
    String outputFile = data.options.getProperty(TRACE_FILE_KEY);
    if (outputFile != null)
    {
      try
      {
        if ("-".equals(outputFile))
        {
          out = DEFAULT_OUTPUT;
        }
        else
        {
          PrintWriter p = new RollingPrintWriter(outputFile);
          out = p;
        }
      }
      catch (IOException e)
      {
        e.printStackTrace(out);
      }
    }

    String url = data.options.getProperty(URL_KEY);
    if (url == null)
      throw new WrapperException("Missing \"url=\" option.");

    String className = data.options.getProperty(CLASS_KEY);
    if (className != null)
    {
      className = className.intern();
      synchronized(driverClasses)
      {
        if (!driverClasses.contains(className))
        {
          driverClasses.add(className);
          try
          {
            Util.trace("Attempting to load class: " + className);
            Class driverClass = Class.forName(className);
            realDriver = (Driver) driverClass.newInstance();

            // See if the new driver is a Sybase driver.
            // Note -- If the Sybase driver isn't in our Classpath
            // then we just skip all this by throwing a ClassNotFoundException.
            Class sybase = Class.forName("com.sybase.jdbc2.jdbc.SybDriver");
            if (sybase.isAssignableFrom(driverClass))
            {
              Class handler = Class.forName(WrapperDriver.class.getPackage().getName() + ".SybaseMessageHandler");
              Class sybMessageHandler = Class.forName("com.sybase.jdbcx.SybMessageHandler");
              Method method = realDriver.getClass().getMethod("setMessageHandler", new Class[] {sybMessageHandler});
              method.invoke(realDriver, new Object[] {handler.newInstance()});
            }
          }
          catch (Throwable e)
          {
            // Ignore errors -- That just means that when we try to
            // register the real JDBC URL, then it might fail.
          }
        }
      }
    }

    String traceValue = data.options.getProperty(TRACE_KEY);
    if (traceValue != null)
    {
      try
      {
        data.trace = Integer.parseInt(traceValue.trim());
        if (data.trace < 0)
          data.trace = 0;
        if (data.trace > 0)
          Util.startTraceThread("Trace", data);
      }
      catch (NumberFormatException e)
      {
        // ignore
      }
    }

    Util.trace("WrapperDriver: Connecting to \"" + url + '"');

    data.connection = new WrapperConnection(DriverManager.getConnection(url, info), data);
    return data.connection;
  }

  public boolean jdbcCompliant()
  {
    if (realDriver == null)
    {
      Util.trace("wrapper jdbcCompliant() -> " + false);
      return false;
    }
    long key = Util.traceBegin("jdbcCompliant()");
    boolean result = realDriver.jdbcCompliant();
    Util.traceEnd(key, String.valueOf(result));
    return result;
  }

  /**
   * Get URL details:
   * jdbc:wrapper:option=value;option=value;...
   *
   * Options:
   * url=xxx   - Full URL of actual driver: jdbc:xxxx:...
   * class=xxx - Class name of actual driver.  Will be loaded if specified.
   * trace     - Whether to trace all calls.
   * report=off | close | nnnn
   *           - Whether to do profile report at connection-close
   *           - or every nnnn milliseconds.  Off or 0 stops profile
   *           - reporting
   * incremental=true | false
   *           - Whether to clear statistics after each report, or continue
   *           - gathering data.
   * profile   - Whether to collect profiling/timing data.
   */

  private Data parseUrl(String url, Properties defaults)
  {
    if (url == null || !url.startsWith(urlPrefix))
      return null;

    url = url.substring(urlPrefix.length());

    Data data = new Data();
    data.options = new Properties(defaults);
    StringTokenizer urlOptions = new StringTokenizer(url, ";");

    while (urlOptions.hasMoreTokens())
    {
      String option = urlOptions.nextToken();
      int split = option.indexOf('=');
      String key;
      String value;

      if (split == -1)
      {
        key = option;
        value = "";
      }
      else
      {
        key = option.substring(0, split);
        value = decode(option.substring(split + 1));
      }

      data.optionList.add(new StringPair(key, value));
      data.options.setProperty(key, value);
    }

    return data;
  }

  /**
   * URL-decode the string.  Only deals with %HH sequences.
   */
  private static String decode(String value)
  {
    if (value == null)
      return null;

    int percent = value.indexOf('%');
    if (percent == -1)
      return value;

    StringBuffer sb = new StringBuffer(value.length());
    sb.append(value.substring(0, percent));
    char[] v = value.substring(percent).toCharArray();

    int i = 0;

    do
    {
      int left;
      int right;

      if (v[i] == '%' && i + 2 < v.length &&
          (left = Character.getNumericValue(v[i+1])) >= 0 && left <= 15  &&
          (right = Character.getNumericValue(v[i+2])) >= 0 && right <= 15)
      {
        sb.append((char)(left * 16 + right));
        i += 2;

      }
      else
        sb.append(v[i]);

    } while (++i < v.length);

    return sb.toString();
  }


  //////////////////////////////////////////////////////////////////////
  // DataSource implementation

  public Connection getConnection() throws SQLException
  {
    long key = 0L;
    if (connectionData.trace >= 1)
      key = Util.traceBegin("DataSource.getConnection()");

    try
    {
      if (connectionData.options == null)
        throw new SQLException("Cannot call DataSource.getConnection() now: No JDBC parameters set up");

      WrapperConnection result = connect(connectionData.copy(), new Properties());

      if (connectionData.trace >= 1)
        Util.traceEnd(key, result.prefix);
      return result;
    }
    catch  (SQLException e)
    {
      if (connectionData.trace >= 1)
        Util.traceEnd(key, e);
      throw e;
    }
  }

  public Connection getConnection(String user, String password) throws SQLException
  {
    long key = 0L;
    if (connectionData.trace >= 1)
      key = Util.traceBegin("DataSource.getConnection(" + user + ", ********)");

    try
    {
      if (connectionData.options == null)
        throw new SQLException("Cannot call DataSource.getConnection() now: No JDBC parameters set up");

      Properties info = new Properties();

      if (user != null)
      {
        info.put("user", user);
      }
      if (password != null)
      {
        info.put("password", password);
      }
      WrapperConnection result = connect(connectionData.copy(), info);

      if (connectionData.trace >= 1)
        Util.traceEnd(key, result.prefix);
      return result;
    }
    catch  (SQLException e)
    {
      if (connectionData.trace >= 1)
        Util.traceEnd(key, e);
      throw e;
    }
  }

  public PrintWriter getLogWriter() throws SQLException
  {
    if (connectionData.trace >= 1)
      Util.trace("DataSource.getLogWriter() -> " + (out == DEFAULT_OUTPUT ? "System.err" : out.getClass().getName()));

    return out;
  }

  public void setLogWriter(PrintWriter out) throws SQLException
  {
    String line = null;
    if (connectionData.trace >= 1)
      line = "DataSource.setLogWriter(" + (out == null ? "null => System.err" : out.getClass().getName()) + ")";

    if (line != null)
      Util.trace(line);

    if (out == null)
      out = DEFAULT_OUTPUT;

    WrapperDriver.out = out;

    if (line != null)
      Util.trace(line);
  }

  public void setLoginTimeout(int seconds) throws SQLException
  {
    if (connectionData.trace >= 1)
      Util.trace("DataSource.setLoginTimeout(" + seconds + ")");

    loginTimeout = seconds;
  }

  public int getLoginTimeout() throws SQLException
  {
    if (connectionData.trace >= 1)
      Util.trace("DataSource.getLoginTimeout() -> " + loginTimeout);

    return loginTimeout;
  }
}

class StringPair
{
  final String key;
  final String value;

  StringPair(String key, String value)
  {
    this.key = key;
    this.value = value;
  }
}

//class SybaseMessageHandler implements com.sybase.jdbcx.SybMessageHandler
//{
//  final String tranState[] = {"extended transaction", "committed", "2", "aborted"};
//
//  public SQLException messageHandler(SQLException sqe)
//  {
//    if (sqe instanceof com.sybase.jdbcx.EedInfo)
//    {
//      com.sybase.jdbcx.EedInfo e = (com.sybase.jdbcx.EedInfo) sqe;
//      Util.trace("**** Sybase Exception: class: " + e.getClass().getName() +
//                 ", message: " + sqe.getMessage() +
//                 ", status=" + e.getStatus() +
//                 ", lineNumber="    + e.getLineNumber() +
//                 ", procedureName=" + e.getProcedureName() +
//                 ", serverName="    + e.getServerName() +
//                 ", severity="      + e.getSeverity() +
//                 ", state="         + e.getState() +
//                 ", tranState="     + tranState[e.getTranState()]);
//
//    }
//
//    // int code = sqe.getErrorCode();
//    // if (code == AVOID_SQLE)
//    // {
//    //   System.out.println("User " + _user + " downgrading " +
//    //                      AVOID_SQLE + " to a warning");
//    //   sqe = new SQLWarning(sqe.getMessage(),
//    //                        sqe.getSQLState(),sqe.getErrorCode());
//    // }
//
//    return sqe;
//  }
//}
